<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">

<title reclang="CS4l">RecordApp QuickStart: 快速入门</title>
<script>var PageLM="2024-09-09 21:58";</script>
<!--
你可以直接将 /app-support-sample/QuickStart.html 文件copy到你的(https)网站中，无需其他文件，就能正常开始测试了。
You can directly copy the /app-support-sample/QuickStart.html file to your (https) website, and you can start testing normally without other files.
-->

<script> //CDN URL 可删掉，选择线路用的
var GitPageBase="https://xiangyuecn.github.io/Recorder/";
if(/gitee\.io/.test(location.host)){ //2024-05-01 pages无通知下线
	GitPageBase="https://xiangyuecn.gitee.io/recorder/";
};
document.write('<base href="'+GitPageBase+'app-support-sample/" />');
document.write('<link rel="shortcut icon" type="image/png" href="'+GitPageBase+'assets/icon.png">');
</script>

</head>

<body>
<!--
【1】引入框架文件  Import plugin file   ，注意自己使用时应当自己把源码clone下来，然后通过src="/src/recorder-core.js"引入

另外：[1.1]、[1.2]可以合并为使用"/recorder.mp3.min.js"，这个文件为压缩版大幅减小文件体积，已经包含了这3个源码文件
-->

<!-- 【1.1】引入核心文件  Import core file -->
<script src="改成你clone的源码目录/../../src/recorder-core.js"></script>

<!-- 【1.2】引入相应格式支持文件；如果需要多个格式支持，把这些格式的编码引擎js文件放到后面统统加载进来即可。  Import the corresponding format support files; if you need multiple format support, put the encoding engine js files of these formats in the back and load them all. -->
<script src="改成你clone的源码目录/../../src/engine/mp3.js"></script>
<script src="改成你clone的源码目录/../../src/engine/mp3-engine.js"></script>

<!-- 【1.3】引入可选的扩展支持项，如果不需要这些扩展功能可以不引入。  Import optional extended support items. If you do not need these extended functions, you do not need to import them. -->
<script src="改成你clone的源码目录/../../src/extensions/frequency.histogram.view.js"></script>
<script src="改成你clone的源码目录/../../src/extensions/lib.fft.js"></script>

<!-- 【1.4】引入RecordApp  Import RecordApp -->
<script src="改成你clone的源码目录/../../src/app-support/app.js"></script>
<!-- 【1.5】引入Native支持文件，用于在Android、iOS App中调用原生录音。  Import Native support files for calling native recording in Android and iOS Apps. -->
<script src="改成你clone的源码目录/../../src/app-support/app-native-support.js"></script>
<!-- 【1.6】引入Native测试配置文件（实现Hybrid App的JsBridge调用）。  Import the Native test configuration file (implement the JsBridge call of the Hybrid App). -->
<script src="改成你clone的源码目录/../native-config.js"></script>


<!-- 【*】无关js，仅供本页面用的国际化多语言支持，可删除。  It has nothing to do with js, it is only for international multilingual support for this page, it can be deleted. -->
<script src="改成你clone的源码目录/../../assets/ztest-page-i18n.js"></script>
<script src="改成你clone的源码目录/../../assets/zdemo.widget.donate.js"></script>

<script>var bEL=document.querySelector("base");if(bEL)bEL.parentNode.removeChild(bEL);//清除CDN地址  Clear CDN URLs</script>


<!-- 【2】构建界面  Build the web interface  -->
<div class="main">
	<div class="i18nBox"></div>
	<div class="mainBox topLinks">
		<span style="font-size:32px;color:#f60;" reclang="47ME">Recorder App QuickStart: 快速入门</span>
		<a href="https://github.com/xiangyuecn/Recorder" target="_blank">GitHub</a>
		| <a href="https://gitee.com/xiangyuecn/Recorder" target="_blank">Gitee</a>
		
		<div style="padding-top:10px;color:#666">
			<span reclang="0KbD">更多Demo：</span>
			<a class="lb" href="https://xiangyuecn.github.io/Recorder/app-support-sample/" target="_blank">Recorder App</a>
			<a class="lb" href="https://xiangyuecn.github.io/Recorder/assets/demo-vue/recordapp.html" target="_blank">App vue</a>
			<a class="lb" href="https://xiangyuecn.github.io/Recorder/" target="_blank">Recorder H5</a>
			<a class="lb" href="https://xiangyuecn.github.io/Recorder/QuickStart.html" target="_blank">H5 QuickStart</a>
			<a class="lb" href="https://xiangyuecn.github.io/Recorder/assets/demo-vue/" target="_blank">H5 vue</a>
		</div>
	</div>
	
	<div class="mainBox">
		<!-- 按钮控制区域  Control button area -->
		<div class="btns">
			<button onclick="recReq()" reclang="65pE">请求权限</button>
			<button onclick="recStart()" reclang="B3ly">录制</button>
			<button onclick="recStop()" reclang="qpRJ">停止</button>
		</div>
		<div class="pd btns">
			<span style="display: inline-block;margin-right: 36px;">
				<button onclick="recPause()" reclang="hQ6r">暂停</button>
				<button onclick="recResume()" reclang="ncRU">继续</button>
				<button onclick="recStopX()" reclang="V0wV">停止(仅清理)</button>
			</span>
			
			<span style="display: inline-block;">
				<button onclick="recPlay()" reclang="uwNo">播放</button>
				<button onclick="recUpload()" reclang="ziQt">上传</button>
				<button onclick="recLocalDown()" reclang="HsFg">本地下载</button>
			</span>
		</div>
		
		<!-- 波形绘制区域  Waveform drawing area -->
		<div class="pd recpower">
			<div style="height:40px;width:300px;background:#999;position:relative;">
				<div class="recpowerx" style="height:40px;background:#0B1;position:absolute;"></div>
				<div class="recpowert" style="padding-left:50px; line-height:40px; position: relative;"></div>
			</div>
		</div>
		<div class="pd waveBox">
			<div style="border:1px solid #ccc;display:inline-block"><div style="height:100px;width:300px;" class="recwave"></div></div>
		</div>
		
		<!-- 功能配置区域 Function configuration area -->
		<div>
			<div>
				<span class="lb">AppUseH5 :</span> <label><input type="checkbox" class="alwaysAppUseH5"><span reclang="iIYH">App里面总是使用Recorder H5录音</span></label>
			</div>
		</div>
	</div>
	
	<!-- 日志输出区域  Log output area -->
	<div class="mainBox">
		<div class="reclog"></div>
	</div>
</div>


<!-- 【3】实现录音逻辑  Implement recording logic -->
<script>
var wave,recBlob;
/**调用RequestPermission打开录音请求好录音权限  Call RequestPermission to open the recording and request the recording permission.**/
var recReq=function(){//一般在显示出录音按钮或相关的录音界面时进行此方法调用，后面用户点击开始录音时就能畅通无阻了
	reclog(Html_$T("Dy0d::开始请求授权..."));
	
	RecordApp.RequestPermission(function(){
		reclog(RecordApp.Current.Key+" "+Html_$T("crXf::已授权"),2);
	},function(err,isUserNotAllow){
		reclog((RecordApp.Current&&RecordApp.Current.Key||"[?]")
			+(isUserNotAllow?" UserNotAllow, ":"")
			+" "+Html_$T("z8Lp::授权失败：")+err,1);
	});
};



/**开始录音  Start recording**/
function recStart(){
	if(!RecordApp.Current){
		reclog(Html_$T("38jY::未请求权限"), 1);
		return;
	};
	
	if(RecordApp.Current==RecordApp.Platforms.Native){
		reclog(Html_$T("oZxa::正在使用Native录音，底层由App原生层提供支持"));
	}else{
		reclog(Html_$T("l9Th::正在使用H5录音，底层由Recorder直接提供支持"));
	};
	
	var set={
		type:"mp3"
		,bitRate:16
		,sampleRate:16000
		//,audioTrackSet:{echoCancellation:true,noiseSuppression:true,autoGainControl:true} //配置回声消除，注意：H5中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
		,onProcess:function(buffers,powerLevel,bufferDuration,bufferSampleRate,newBufferIdx,asyncEnd){
			//录音实时回调，大约1秒调用12次本回调
			document.querySelector(".recpowerx").style.width=powerLevel+"%";
			document.querySelector(".recpowert").innerText=formatMs(bufferDuration,1)+" / "+powerLevel;
			processTime=Date.now();
			
			//可视化图形绘制
			wave.input(buffers[buffers.length-1],powerLevel,bufferSampleRate);
		}
	};
	
	wave=null;
	recBlob=null;
	var processTime=0;
	reclog(RecordApp.Current.Key+" "+Html_$T("ULVy::正在打开..."));
	RecordApp.Start(set,function(){
		reclog(RecordApp.Current.Key+" "+Html_$T("qUcx::录制中：")+set.type+" "+set.sampleRate+" "+set.bitRate+"kbps",2);
		
		//此处创建这些音频可视化图形绘制浏览器支持妥妥的
		wave=Recorder.FrequencyHistogramView({elem:".recwave"});
		
		//【稳如老狗WDT】可选的，监控是否在正常录音有onProcess回调，如果长时间没有回调就代表录音不正常
		var this_=   RecordApp; //有this就用this，没有就用一个全局对象
		if(RecordApp.Current.CanProcess()){
			var wdt=this_.watchDogTimer=setInterval(function(){
				if(wdt!=this_.watchDogTimer){ clearInterval(wdt); return } //sync
				if(Date.now()<this_.wdtPauseT) return; //如果暂停录音了就不检测：puase时赋值this_.wdtPauseT=Date.now()*2（永不监控），resume时赋值this_.wdtPauseT=Date.now()+1000（1秒后再监控）
				if(Date.now()-(processTime||startTime)>1500){ clearInterval(wdt);
					reclog(processTime?Html_$T("eWy1::录音被中断"):Html_$T("eWy2::录音未能正常开始"),1);
					// ... 错误处理，关闭录音，提醒用户
				}
			},1000);
		}else{
			reclog(Html_$T("eWy3::当前环境不支持onProcess回调，不启用watchDogTimer"),"#aaa"); //目前都支持回调
		}
		var startTime=Date.now(); this_.wdtPauseT=0;
	},function(err){
		reclog(RecordApp.Current.Key+" "+Html_$T("0Rxw::开始录音失败：")+err,1);
	});
};

/**暂停录音  Passing recording**/
function recPause(){
	if(RecordApp.GetCurrentRecOrNull()){
		RecordApp.Pause();
		var this_=RecordApp;this_.wdtPauseT=Date.now()*2; //永不监控onProcess超时
		reclog(Html_$T("S27N::已暂停"));
	}
};
/**恢复录音  Resume recording**/
function recResume(){
	if(RecordApp.GetCurrentRecOrNull()){
		RecordApp.Resume();
		var this_=RecordApp;this_.wdtPauseT=Date.now()+1000; //1秒后再监控onProcess超时
		reclog(Html_$T("ChUt::继续录音中..."));
	}
};

/**结束录音，得到音频文件  Stop recording and get audio files**/
function recStop(){
	if(!RecordApp.Current){
		reclog(Html_$T("eHzv::未请求权限"),1);
		return;
	};
	
	var this_=RecordApp;this_.watchDogTimer=0; //停止监控onProcess超时
	RecordApp.Stop(function(aBuf,duration,mime){
		var blob=new Blob([aBuf],{type:mime});
		console.log(blob,(window.URL||webkitURL).createObjectURL(blob),"duration:"+duration+"ms");
		
		recBlob=blob;
		reclog(Html_$T("YcB5::已录制mp3：{1}ms {2}字节，可以点击播放、上传、本地下载了",0,formatMs(duration),blob.size),2);
	},function(msg){
		reclog(Html_$T("nIFF::录音失败：")+msg,1);
	});
};
function recStopX(){
	var this_=RecordApp;this_.watchDogTimer=0; //停止监控onProcess超时
	RecordApp.Stop(
		null //success传null就只会清理资源，不会进行转码
		,function(msg){
			reclog(Html_$T("vHlR::已清理，错误信息：")+msg);
		}
	);
};









/**播放  Play**/
function recPlay(){
	if(!recBlob){
		reclog(Html_$T("kWpA::请先录音，然后停止后再播放"),1);
		return;
	};
	var cls=("a"+Math.random()).replace(".","");
	reclog(Html_$T('XCKe::播放中: ')+'<span class="'+cls+'"></span>');
	var audio=document.createElement("audio");
	audio.controls=true;
	document.querySelector("."+cls).appendChild(audio);
	//简单利用URL生成播放地址，注意不用了时需要revokeObjectURL，否则霸占内存
	audio.src=(window.URL||webkitURL).createObjectURL(recBlob);
	audio.play();
	
	setTimeout(function(){
		(window.URL||webkitURL).revokeObjectURL(audio.src);
	},5000);
};

/**上传  Upload**/
function recUpload(){
	var blob=recBlob;
	if(!blob){
		reclog(Html_$T("SLaX::请先录音，然后停止后再上传"),1);
		return;
	};
	
	//本例子假设使用原始XMLHttpRequest请求方式，实际使用中自行调整为自己的请求方式
	//录音结束时拿到了blob文件对象，可以用FileReader读取出内容，或者用FormData上传
	var api="http://127.0.0.1:9528";
	var onreadystatechange=function(xhr,title){
		return function(){
			if(xhr.readyState==4){
				if(xhr.status==200){
					reclog(title+Html_$T("DHfL::上传成功")+' <span style="color:#999">response: '+xhr.responseText+'</span>',2);
				}else{
					reclog(title+Html_$T("K1x7::没有完成上传，演示上传地址无需关注上传结果，只要浏览器控制台内Network面板内看到的请求数据结构是预期的就ok了。"), "#d8c1a0");
					
					console.error(Html_xT(title+Html_$T("UKGO::上传失败")),xhr.status,xhr.responseText);
				};
			};
		};
	};
	reclog(Html_$T("JIr4::开始上传到{1}，请稍候... （你可以先到源码 /assets/node-localServer 目录内执行 npm run start 来运行本地测试服务器）",0,api));

	/***方式一：将blob文件转成base64纯文本编码，使用普通application/x-www-form-urlencoded表单上传***/
	var reader=new FileReader();
	reader.onloadend=function(){
		var postData="";
		postData+="mime="+encodeURIComponent(blob.type);//告诉后端，这个录音是什么格式的，可能前后端都固定的mp3可以不用写
		postData+="&upfile_b64="+encodeURIComponent((/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result)||[])[1]) //录音文件内容，后端进行base64解码成二进制
		//...其他表单参数
		
		var xhr=new XMLHttpRequest();
		xhr.open("POST", api+"/uploadBase64");
		xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
		xhr.onreadystatechange=onreadystatechange(xhr,Html_$T("XVmd::上传方式一【Base64】"));
		xhr.send(postData);
	};
	reader.readAsDataURL(blob);

	/***方式二：使用FormData用multipart/form-data表单上传文件***/
	var form=new FormData();
	form.append("upfile",blob,"recorder.mp3"); //和普通form表单并无二致，后端接收到upfile参数的文件，文件名为recorder.mp3
	//...其他表单参数
	
	var xhr=new XMLHttpRequest();
	xhr.open("POST", api+"/upload");
	xhr.onreadystatechange=onreadystatechange(xhr,Html_$T("6rhE::上传方式二【FormData】"));
	xhr.send(form);
};


/**本地下载  Local download**/
function recLocalDown(){
	if(!recBlob){
		reclog(Html_$T("hFmL::请先录音，然后停止后再下载"),1);
		return;
	};
	var cls=("a"+Math.random()).replace(".","");
	recdown64.lastCls=cls;
	reclog(Html_$T('UrgF::点击 ')+'<span class="'+cls+'"></span>'+Html_$T('c7b7:: 下载，或复制文本')
		+'<button onclick="recdown64(\''+cls+'\')">'+Html_$T('6Bln::生成Base64文本')+'</button><span class="'+cls+'_b64"></span>');
	
	var fileName="recordapp-"+Date.now()+".mp3";
	var downA=document.createElement("A");
	downA.innerHTML=Html_$T("ROdY::下载 ")+fileName;
	downA.href=(window.URL||webkitURL).createObjectURL(recBlob);
	downA.download=fileName;
	document.querySelector("."+cls).appendChild(downA);
	if(/mobile/i.test(navigator.userAgent)){
		alert(Html_xT(Html_$T("3rPj::因移动端绝大部分国产浏览器未适配Blob Url的下载，所以本demo代码在移动端未调用downA.click()。请尝试点击日志中显示的下载链接下载")));
	}else{
		downA.click();
	}
	
	//不用了时需要revokeObjectURL，否则霸占内存
	//(window.URL||webkitURL).revokeObjectURL(downA.href);
};
function recdown64(cls){
	var el=document.querySelector("."+cls+"_b64");
	if(recdown64.lastCls!=cls){
		el.innerHTML='<span style="color:red">'+Html_$T("weWp::老的数据没有保存，只支持最新的一条")+'</span>';
		return;
	}
	var reader = new FileReader();
	reader.onloadend = function() {
		el.innerHTML='<textarea></textarea>';
		el.querySelector("textarea").value=reader.result;
	};
	reader.readAsDataURL(recBlob);
};










var formatMs=function(ms,all){
	var ss=ms%1000;ms=(ms-ss)/1000;
	var s=ms%60;ms=(ms-s)/60;
	var m=ms%60;ms=(ms-m)/60;
	var h=ms;
	var t=(h?h+":":"")
		+(all||h+m?("0"+m).substr(-2)+":":"")
		+(all||h+m+s?("0"+s).substr(-2)+"″":"")
		+("00"+ss).substr(-3);
	return t;
};
</script>








<!--以下这坨可以忽略  The following can be ignored-->
<script>
function reclog(s,color){
	var now=new Date();
	var t=("0"+now.getHours()).substr(-2)
		+":"+("0"+now.getMinutes()).substr(-2)
		+":"+("0"+now.getSeconds()).substr(-2);
	var div=document.createElement("div");
	var elem=document.querySelector(".reclog");
	elem.insertBefore(div,elem.firstChild);
	div.innerHTML='<div style="color:'+(!color?"":color==1?"red":color==2?"#0b1":color)+'">['+t+']'+s+'</div>';
};
window.onerror=function(message, url, lineNo, columnNo, error){
	//https://www.cnblogs.com/xianyulaodi/p/6201829.html
	reclog('<span style="color:red">【Uncaught Error】'+message+'<pre>'+"at:"+lineNo+":"+columnNo+" url:"+url+"\n"+(error&&error.stack||Html_$T("L6RO::不能获得错误堆栈"))+'</pre></span>');
};

if(!window.Html_$T){//没有提供本页面用的国际化多语言支持时 返回中文文本
	window.Html_$T=function(){
		var a=arguments,txt=a[0].replace(/^.+?::/,""),n=0;
		for(var i=0;i<a.length;i++){ if(typeof a[i]=="number"){ n=i;break } }
		txt=txt.replace(/\{(\d+)\}/g,function(v,f){ v=a[+f+n]; return v==null?"":v });
		return txt;
	}
	window.Html_xT=function(v){ return v }
}

reclog(Html_$T('FaWz::如需录音功能定制开发，网站、App、小程序、前端后端开发等需求，请加QQ群：①群 781036591、②群 748359095、③群 450721519，口令recorder，联系群主（即作者），谢谢~'),"#333;font-size:22px;font-weight:bold");
reclog(Html_$T("98Ry::Recorder App基于Recorder H5的跨平台录音，支持在浏览器环境中使用（H5）、各种使用js来构建的程序中使用（App、小程序、UniApp、Electron、NodeJs）")+unescape("%uD83C%uDF89"),"#f60;font-weight:bold;font-size:24px");
reclog(Html_$T("p71N::Recorder H5使用简单，功能丰富，支持PC、Android、iOS 14.3+")+unescape("%uD83D%uDCAA"),"#0b1;font-weight:bold;font-size:24px");
reclog(Html_$T('5hyY::本页面修改时间（有可能修改了忘改）：')+PageLM,"#999");
reclog(Html_$T('7GKB::RecordApp库修改时间（有可能修改了忘改）：')+(window.RecordApp&&RecordApp.LM),"#999");
reclog(Html_$T('or9l::Recorder库修改时间（有可能修改了忘改）：')+(window.Recorder&&Recorder.LM),"#999");
reclog("UA: "+navigator.userAgent, "#999");
reclog("URL: "+location.href.replace(/#.*/g,""), "#999");
reclog(Html_$T('veMB::你可以直接将 ')+'<a target="_blank" href="https://github.com/xiangyuecn/Recorder/blob/master/app-support-sample/QuickStart.html">/app-support-sample/QuickStart.html</a>'+Html_$T('EA2M:: 文件copy到你的(https)网站中，无需其他文件，就能正常开始测试了，本文件更适合入门学习')+unescape("%uD83D%uDE04"));


if(window.RecordApp){
	document.querySelector(".alwaysAppUseH5")
		.addEventListener("click",function(e){
			RecordApp.AlwaysAppUseH5=e.target.checked;
			RecordApp.Current=null;
			reclog(Html_$T("Xvin::AppUseH5选项变更，已重置RecordApp，请先进行权限测试"));
		});
	
	//立即加载环境
	RecordApp.Install(function(){
		reclog(Html_$T("1GmN::Install成功，环境：")+RecordApp.Current.Key,2);
		reclog(Html_$T('1ffD::页面已准备好，请先点击请求权限，然后点击录制'),2);
	},function(err){
		reclog(Html_$T("Y5vJ::RecordApp.Install出错：")+err,1);
	});
}else{
	reclog(Html_$T("1ISU::js文件加载失败，请刷新重试！"),"#f00;font-size:50px");
}
</script>

<script>
//修改a链接的url
var els=document.querySelectorAll(".topLinks a");
for(var i=0;i<els.length;i++){
	var el=els[i],v=el.getAttribute("href")||"";
	if(/\.io\/Recorder\/(.*)/i.test(v)){
		el.setAttribute("href", GitPageBase+RegExp.$1);
	}
}

if(/mobile/i.test(navigator.userAgent)){
	//移动端加载控制台组件
	var elem=document.createElement("script");
	elem.setAttribute("type","text/javascript");
	elem.setAttribute("src",GitPageBase+"assets/ztest-vconsole.js");
	document.body.appendChild(elem);
	elem.onload=function(){
		new VConsole();
	};
};
</script>

<!-- 加载打赏挂件 -->
<script>
var donateView=document.createElement("div");
document.querySelector(".reclog").appendChild(donateView);
DonateWidget({
	log:function(msg){reclog(msg)}
	,mobElem:donateView
});
</script>

<!-- 启用国际化多语言支持 -->
<script>
if(window.PageI18nWidget){
PageI18nWidget({
	elem:".i18nBox", rootUrl:GitPageBase
	,titleKey:"CS4l"
	,langs:{
		"en-US":{urls:[ "#app_QuickStart_html/en-US.js","#widget_donate/en-US.js" ]}
	}
});
};
</script>

<style>
body{
	word-wrap: break-word;
	background:#f5f5f5 center top no-repeat;
	background-size: auto 680px;
}
pre{
	white-space:pre-wrap;
}
a{
	text-decoration: none;
	color:#06c;
}
a:hover{
	color:#f00;
}

.main{
	max-width:700px;
	margin:0 auto;
	padding-bottom:80px
}

.mainBox{
	margin-top:12px;
	padding: 12px;
	border-radius: 6px;
	background: #fff;
	--border: 1px solid #f60;
	box-shadow: 2px 2px 3px #aaa;
}


.btns button{
	display: inline-block;
	cursor: pointer;
	border: none;
	border-radius: 3px;
	background: #f60;
	color:#fff;
	padding: 0 15px;
	margin:3px 20px 3px 0;
	line-height: 36px;
	height: 36px;
	overflow: hidden;
	vertical-align: middle;
}
.btns button:active{
	background: #f00;
}

.pd{
	padding:0 0 6px 0;
}
.lb{
	display:inline-block;
	vertical-align: middle;
	background:#00940e;
	color:#fff;
	font-size:14px;
	padding:2px 8px;
	border-radius: 99px;
}
</style>

</body>
</html>